/* kroute.c -- Add/remove routes to the kernel
 *
 * Most of the code here is adapted from Asanga Udugama's HOWTO for rtnetlink.
 * See his website: http://www.comnets.uni-bremen.de/~adu/
 *
 * The error checking code is mostly from libnetlink.c, part of the
 * iproute2 package.
 *
 * TODO: We "should" use the rtnetlink macros to add attributes, etc.,
 * but currently we do not.  In the future, it would be nice to use the
 * libnetlink library for all of this, which is a much cleaner interface.
 */

#include <stdio.h>
#include <sys/socket.h>
#include <asm/types.h>
#include <linux/rtnetlink.h>
#include <string.h>
#include <errno.h>

#include "advrp.h"
#include "kroute.h"
#include "debug.h"

/* kernel_route
 * Add, remove, or modify routes installed in the kernel.
 * The "cmd" argument can be ADD, DEL, or MOD, defined in advrp.h.
 * The "local" argument should be either 1 or 0, and describes whether the route
 * is directly connected; usually you should use the * LOCAL(neighbor)
 * macro to populate this (see receiver.c).
 * If it is directly connected, the system should already have a route for it,
 * so we'll just install a throw route for these routes.
 */
int kernel_route(internalRoute * route, int cmd, int local)
{
	struct {
		struct nlmsghdr nl;
		struct rtmsg rt;
		char buf[8192];
	} req;
	char rbuf[8192];                  /* Reply buffer for errors */
	struct nlmsghdr * h;
	struct rtattr * attr;
	int fd;
	int len;
	struct sockaddr_nl laddr, paddr;  /* local, peer address */
	struct msghdr msg;
	struct iovec iov;
	int flags = 0;
	int type = 0;
	int ret;

	/* Make the message */
	bzero(&req, sizeof(req));
	len = sizeof(struct rtmsg);
	attr = (struct rtattr *) req.buf;

	switch (cmd) {
		case ADD:
			DBG(KERN_1, "Received an ADD request.");
			flags |= NLM_F_CREATE;
			type |= RTM_NEWROUTE;
			if (local) {
				DBG(KERN_1, "Route was local; adding a throw "
				    "route.");
				req.rt.rtm_type = RTN_THROW;
				break;
			}

			/* Add other route attributes if not local */
			req.rt.rtm_type = RTN_UNICAST;

			/* Exit interface */
			attr = (struct rtattr *) (((char *) attr) +
			        attr->rta_len);
			attr->rta_type = RTA_OIF;
			attr->rta_len = sizeof(struct rtattr) + 4;
			memcpy(((char *) attr) + sizeof(struct rtattr),
			       (int *) &route->neigh->iface->ifnumber, 1);
			len += attr->rta_len;
		
			/* Next hop */
			attr = (struct rtattr *) (((char *) attr) +
			        attr->rta_len);
			attr->rta_type = RTA_GATEWAY;
			attr->rta_len = sizeof(struct rtattr) + 4;
			memcpy(((char *) attr) + sizeof(struct rtattr),
			       (int *) &route->neigh->addr, 4);
			len += attr->rta_len;

			/* Metric */
			attr = (struct rtattr *) (((char *) attr) +
			        attr->rta_len);
			attr->rta_type = RTA_PRIORITY;
			attr->rta_len = sizeof(struct rtattr) + 4;
			memcpy(((char *) attr) + sizeof(struct rtattr),
			       (int *) &route->route->metric, 1);
			len += attr->rta_len;
			
			break;
		case DEL:
			DBG(KERN_1, "Received a DEL request.");
			flags |= NLM_F_CREATE;
			type |= RTM_DELROUTE;
			break;
		case MOD:
			DBG(KERN_1, "Received a MOD request.");
			flags |= NLM_F_REPLACE;
			type |= RTM_NEWROUTE;
			break;
		default:
			DBG(DEV_1, "Unknown cmd passed to kernel_route: %d.",
			    cmd);
			return 1;
			break;
	}

	if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
		DBG(DEV_1, "Socket call failed.\n");
		return 1;
	}

	memset(&laddr, 0, sizeof(laddr));
	laddr.nl_family = AF_NETLINK;

	if (bind(fd, (struct sockaddr *) &laddr, sizeof(laddr)) < 0) {
		DBG(DEV_1, "Cannot bind netlink socket.");
		close(fd);
		return 1;
	}

	DBG(KERN_1, "Route attributes: %s/%d in %d via %s dev %s",
	    route->route->netstr, route->route->preflen,
	    route->route->metric, route->neigh->ipstr, 
	    route->neigh->iface->ifname);

	/* Add the dst network attribute to all messages */
	attr = (struct rtattr *) (((char *) attr) + attr->rta_len);
	attr->rta_type = RTA_DST;
	attr->rta_len = sizeof(struct rtattr) + 4;
	memcpy(((char *) attr) + sizeof(struct rtattr), &route->route->network,
	        4);
	len += attr->rta_len;

	/* Set up netlink header */
	req.nl.nlmsg_len = NLMSG_LENGTH(len);
	req.nl.nlmsg_flags = NLM_F_REQUEST | flags;
	req.nl.nlmsg_type = type;

	/* Set up rtnetlink message */
	req.rt.rtm_family = AF_INET;
	req.rt.rtm_table = RT_TABLE_ADVRP;
	req.rt.rtm_protocol = RTPROT_STATIC;
	req.rt.rtm_scope = RT_SCOPE_UNIVERSE;
	req.rt.rtm_dst_len = route->route->preflen;

	/* Set up the message header and peer address */
	memset(&msg, 0, sizeof(msg));
	msg.msg_name = (void *) &paddr;
	msg.msg_namelen = sizeof(paddr);
	iov.iov_base = (void *) &req.nl;
	iov.iov_len = req.nl.nlmsg_len;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;

	memset(&paddr, 0, sizeof(paddr));
	paddr.nl_family = AF_NETLINK;

	/* Send the message */
	if (sendmsg(fd, &msg, 0) <= 0) {
		DBG(DEV_1, "Could not send route message to kernel.");
		close(fd);
		return 1;
	}

	/* Check for errors */
	errno = 0;
	if ((ret = recv(fd, &rbuf, sizeof(rbuf), MSG_DONTWAIT)) == -1) {
		/* EAGAIN is set if no message is waiting for us */
		if (errno == EAGAIN) {
			close(fd);
			return 0;
		} else {
			DBG(ERR|DEV_1, "Could not receive response from "
			    "kernel.");
			close(fd);
			return 1;
		}
	}

	h = (struct nlmsghdr *) rbuf;
	if (NLMSG_OK(h, ret) && h->nlmsg_type == NLMSG_ERROR) {
		struct nlmsgerr * err = (struct nlmsgerr *) NLMSG_DATA(h);
		DBG(ERR|KERN_1, "Kernel responded: %s.", strerror(-err->error));
		close(fd);
		return 1;
	}

	close(fd);
	return 0;
}
